# -*- coding: utf-8 -*-

import logging
import traceback
from collections import OrderedDict

from werkzeug.security import generate_password_hash, check_password_hash

from mongoengine import Document, DynamicDocument, fields
from bson import ObjectId
from webutils_wechat.exceptions import AuthException

from utils.common import now_ts, sentinel

logger = logging.getLogger(__name__)


class Permission(object):
    READ_DATA = 0x01
    ALL = 0x10


class Role(Document):
    name = fields.StringField(required=True)
    permission = fields.IntField(required=True)

    status = fields.IntField(required=True, default=1)
    created_ts = fields.IntField(required=True, default=now_ts)

    roles = OrderedDict(
        guest=Permission.READ_DATA,
        admin=Permission.ALL,
    )

    meta = {
        'collection': 'role',
        'indexes': [
            {
                'fields': ['name'],
            },
        ],
    }

    @classmethod
    def load(cls):
        for role_name, perm in cls.roles.items():
            role = cls.objects(name=role_name).first()
            if role is None:
                role = cls(name=role_name, permission=perm)
                role.save()

    def __repr__(self):
        return '<{}: {}>'.format(self.name, hex(self.permission))


class User(DynamicDocument):
    openid = fields.StringField(default=None)
    unionid = fields.StringField()
    wxid = fields.StringField()
    nickname = fields.StringField()
    avatar = fields.StringField()
    gender = fields.IntField()
    city = fields.StringField()
    province = fields.StringField()
    country = fields.StringField()
    language = fields.StringField()

    session_key = fields.StringField()

    mobile = fields.StringField()

    last_login_ts = fields.IntField(required=True, default=now_ts)
    last_ping_ts = fields.IntField(required=True, default=now_ts)

    status = fields.IntField(required=True, default=1)
    created_ts = fields.IntField(required=True, default=now_ts)

    meta = {
        'collection': 'user',
        'indexes': [
            {
                'fields': ['openid'],
            },
            {
                'fields': ['unionid'],
            },
        ],
    }

    @property
    def redis_payload(self):
        return {
            'pk': str(self.pk),
            'openid': self.openid,
            'unionid': self.unionid,
            'nickname': self.nickname,
            'avatar': self.avatar,
            'city': self.city,
            'province': self.province,
            'country': self.country,
            'language': self.language,
            'session_key': self.session_key,
            'mobile': self.mobile,
            'user_type': self.user_type,
        }

    @property
    def is_admin(self):
        return False

    @property
    def user_type(self):
        return 'wechat'

    @property
    def uid(self):
        return str(self.pk)

    @classmethod
    def login(cls, session_info):
        openid = session_info.get('openid')
        session_key = session_info.get('session_key')

        if openid is None:
            logger.error(
                'No openid found in session info. EXIT LOGIN. session_info: %s',
                session_info,
            )
            raise AuthException(
                errcode=981202,
            )
        user = cls.objects(openid=openid).first()
        if user is None:
            user = cls(
                openid=openid,
                session_key=session_key,
            )

        user.last_login_ts = user.last_ping_ts = now_ts()
        if session_key is not None:
            user.session_key = session_key
        if session_info.get('unionid'):
            logger.info('Updating wechat unionid: %s -> %s', session_info['unionid'], openid)
            user.unionid = session_info['unionid']
        try:
            user.save()
        except:
            logger.error(
                'Creating user error. session_info: %s Err: %s',
                session_info,
                traceback.format_exc(),
            )
            raise AuthException(
                errcode=981251,
            )
        return user

    @classmethod
    def get_proxy(cls, **kwargs):
        if 'pk' not in kwargs:
            logger.error(
                'pk not found in kwargs while getting user proxy %s',
                kwargs,
            )
            return None
        return WechatUserProxy(**kwargs)

    def ping(self):
        try:
            self.update(set__last_ping_ts=now_ts())
        except:
            pass

    def update_profile(self, **kwargs):
        profile = {}
        for k, v in kwargs.items():
            if not v:
                continue
            val = getattr(self, k, sentinel)
            if val is sentinel:
                continue
            if val == v:
                continue
            profile[k] = v
        if profile:
            try:
                self.modify(**profile)
            except:
                raise


class Admin(DynamicDocument):
    username = fields.StringField(required=True, unique=True)
    password_hash = fields.StringField(required=True)

    status = fields.IntField(required=True, default=1)
    created_ts = fields.IntField(required=True, default=now_ts)

    role = fields.ReferenceField('Role')

    meta = {
        'collection': 'admin',
        'indexes': [
            {
                'fields': ['username'],
            },
        ],
    }

    @property
    def password(self):
        raise AttributeError('Not accessible')

    @password.setter
    def password(self, val):
        self.password_hash = generate_password_hash(val)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

    @property
    def is_admin(self):
        return self.role.name == 'admin'

    @property
    def user_type(self):
        return 'admin'

    @property
    def uid(self):
        return str(self.pk)

    @property
    def redis_payload(self):
        return {
            'pk': self.pk,
            'username': self.username,
            'user_type': self.user_type,
            'role_name': getattr(self.role, 'name'),
            'permission': getattr(self.role, 'permission'),
        }

    @classmethod
    def create(cls, username, password, role='admin'):
        admin = cls.objects(username=username).first()
        if admin is not None:
            raise AuthException(
                errcode=981001,
            )
        role = Role.objects(name=role).first()
        if role is None:
            raise AuthException(
                errcode=981002,
            )

        admin = cls(username=username, role=role)
        admin.password = password
        try:
            admin.save()
        except:
            logger.error(
                'Creating admin error. username: %s Err: %s',
                username,
                traceback.format_exc(),
            )
            raise AuthException(
                errcode=981051,
            )
        logger.info(
            'Created admin account for %s role: %s',
            username,
            role,
        )
        return admin

    @classmethod
    def login(cls, username, password):
        if not username:
            raise AuthException(
                errcode=981301
            )
        if not password:
            raise AuthException(
                errcode=981302,
            )
        admin = cls.objects(username=username).first()
        if admin is None:
            logger.warning('Specified admin not exists: %s', username)
            raise AuthException(
                errcode=981303,
            )
        if not admin.verify_password(password):
            logger.warning('Password not match for admin %s -> %s', username, password)
            raise AuthException(
                errcode=981304,
            )
        return admin

    @classmethod
    def get_proxy(cls, **kwargs):
        if 'pk' not in kwargs:
            logger.error(
                'pk not found in kwargs while getting admin proxy! %s',
                kwargs,
            )
            return None
        return AdminProxy(**kwargs)


class BaseUserProxy(object):
    _proxied_class = None
    _proxied_user_type = None

    def __init__(self, user=None, **kwargs):
        self._user = user
        self._proxied_keys = []
        for k, v in kwargs.items():
            if k == 'pk':
                try:
                    v = ObjectId(v)
                except:
                    pass
            self.__dict__[k] = v
            self._proxied_keys.append(k)

    def __getattr__(self, name):
        try:
            val = getattr(self.proxied, name)
        except:
            val = None
        return val

    @property
    def proxied(self):
        if self._user is None:
            self._user = self._proxied_class.objects(pk=self.pk).first()
            if self._user is None:
                logger.error(
                    'PROXIED WECHAT-USER/ADMIN NOT FOUND!! id: %s user_type: %s',
                    self.pk,
                    self._proxied_user_type
                )
            else:
                for k in self._proxied_keys:
                    self.__dict__.pop(k, None)
        return self._user


class WechatUserProxy(BaseUserProxy):
    _proxied_class = User
    _proxied_user_type = 'wechat'

    @property
    def is_admin(self):
        return False


class AdminProxy(BaseUserProxy):
    _proxied_class = Admin
    _proxied_user_type = 'admin'

    @property
    def is_admin(self):
        if self._user:
            return self._user.is_admin
        return self.role_name == 'admin'
